case study_handwriting recognition with hog

Handwriting recognition with HOG


HOG:Histogram of Oriented Gradients。类似于边缘方向直方图和局部不变描述符(例如SIFT),HOG对图像的梯度幅度进行操作。

然而,与SIFT不同,SIFT计算图像的小的局部区域中的边缘方向上的直方图,HOG在均匀间隔的单元的密集网格上计算这些直方图。此外,这些单元也可以重叠并进行对比度归一化,以提高描述符的准确性。

在这种情况下,我们将应用HOG图像描述符和线性支持向量机(SVM)来学习图像数字的表示。

幸运的是,scikit-image库已经实现了HOG描述符,因此在计算其特征表示时我们可以直接使用它。

from skimage import feature 

class HOG:
    def __init__(self,orientations = 9,pixelsPerCell=(8,8),
            cellsPerBlock=(3,3),transform=False):
        # store the number of orientations, pixels per cell,
        # cells per block, and whether or not power law
        # compression should be applied
        self.orienations = orientations
        self.pixelsPerCell = pixelsPerCell
        self.cellsPerBlock = cellsPerBlock
        self.transform = transform

    def describe(self,image):
        # compute HOG for the image
        hist = feature.hog(image,orientations=self.orienations,
            pixels_per_cell=self.pixelsPerCell,
            cells_per_block=self.cellsPerBlock,
            transform_sqrt=self.transform)

        # return the HOG features 
        return hist 

我们首先导入scikit-image的feature子包。该包包含许多从图像中提取特征的方法。

接着,我们设置__init__构造函数,需要四个参数。第一个orientations定义每个直方图中将有多少个梯度方向(即,bins的数量)。pixelsPerCell参数定义将落入每个单元格的像素数。当在图像上计算HOG描述符时,图像将被划分为多个单元,每个单元的大小为pixelsPerCell × pixelsPerCell。然后将为每个单元计算梯度幅度的直方图。

然后,HOG将根据cellsPerBlock参数将落入每个块的单元格数来标准化每个直方图。

可选地,HOG可以应用幂律压缩(获取输入图像的对数/平方根),这可以导致描述符的更好准确性。

在存储了构造函数的参数之后,我们定义了describe方法,只需要一个参数——要计算HOG描述符的图像。

计算HOG描述符由scikit-image的feature子包的hog方法处理。我们传递orientations的数量,每个单元的像素数,每个块的单元格,以及在计算HOG描述符之前是否应该将平方根变换应用于图像。

最后我们将计算的HOG特征向量返回给调用者。

接下来,我们需要一个数字数据集,他可以用来从中提取特征并训练我们的机器学习模型。我们决定使用MNIST数字识别数据集的样本,这是计算机视觉和机器学习文献中的经典数据集。

完整的数据集

数据集的样本由5000个数据点组成,每个数据点具有长度为784的特征向量,对应于图像的28×28灰度像素强度。

但首先,我们需要定义一些方法来帮助我们操作和准备数据集以进行特征提取和训练我们的模型。我们将这些数据集操作函数存储在dataset.py中:

from . import imutils
import numpy as np 
import mahotas
import cv2 

def load_digits(datasetPath):
    # build the dataset and then split it into data
    # and labels
    data = np.genfromtxt(datasetPath,delimiter=",",dtype="uint8")
    target = data[:,0]
    data = data[:,1:].reshape(data.shape[0],28,28)

    # return a tuple of the data and targets 
    return (data,target)

我们首先导入我们需要的包。我们将使用numpy进行数字处理,mahotas是另一个计算机视觉库来辅助cv2,最后是imutils,其中包含执行常见图像处理任务(如调整大小和旋转图像)的便利功能。

为了将我们的数据集加载到磁盘上,我们定义了load_digits方法。该方法只需要一个参数,即datasetPath,它是MNIST样本数据集驻留在磁盘上的路径。

从那里,NumPy的genfromtext函数将数据集加载到磁盘上并将其存储为无符号的8位NumPy数组。请记住,此数据集由图像的像素强度组成。这些像素强度永远不会小于0且绝不会大于255,因此我们能够使用8位无符号整数数据类型。

数据矩阵的第一列包含我们的target,它是图像包含的数字。target将落在[0,9]范围内。

同样,第一个之后的所有列都包含图像的像素强度。同样,这些是尺寸为M×N的数字图像的灰度像素,并且将始终落在[0,255]的范围内。

最后,我们将data和target以元组的形式返回给调用者。

接下来,我们需要对数字图像执行一些预处理:

def deskew(image,width):
    # grab the width and height of the image and compute
    # moments for the image
    (h,w) = image.shape[:2]
    moments = cv2.moments(image)

    # deskew the image by applying an affine transformation
    skew = moments["mu11"] / moments["mu02"]
    M = np.float32([
        [1,skew,-0.5 * w * skew],
        [0,1,0]
    ])
    image = cv2.warpAffine(image,M,(w,h),
        flags=cv2.WARP_INVERSE_MAP | cv2.INTER_LINEAR)

    # resize the image to have a constant width
    image = imutils.resize(image,width = width)

    # return the deskewed image
    return image

每个人都有不同的写作风格。虽然我们大多数人写的数字“向左倾斜”,但有些数字向右倾斜。我们中的一些人以不同的角度写数字。这些变化的角度可能导致试图学习各种数字表示的机器学习模型的混淆。

为了帮助修复一些“lean”数字,我们定义了deskew(倾斜)方法。这个函数有两个参数。第一个是要被歪斜的数字图像。第二个是图像要调整大小的宽度。

我们首先获取图像的高度和宽度,然后计算图像的moment。这些moment包含有关图像中白色像素位置分布的统计信息

根据前面的moments,我们计算出了skew。接着我们构造了warping matrix M。该矩阵M将用于对图像进行去歪斜。

图像的实际偏斜校是调用cv2.warpAffine函数。第一个参数是将要倾斜的图像,第二个参数是定义图像将被歪斜的“方向”的矩阵M,第三个参数是偏斜图像的最终宽度和高度。最后,flags参数控制图像的校正方式。 在这种情况下,我们使用线性插值。

最后我们调整偏斜图像的大小并返回给调用者。

为了获得一致的数字表示,其中所有图像具有相同的宽度和高度,数字位于图像的中心,然后我们需要定义图像的范围:

def center_extent(image,size):
    # grab the extent width and height
    (eW,eH) = size 

    # handle when the width is greater than the height 
    if image.shape[1] > image.shape[0]:
        image = imutils.resize(image,width = eW)
    # otherwise , the height is greater than the width
    else:
        image = imutils.resize(image,height = eH)

    # allocate memory for the extent of the image and 
    # grab it 
    extent = np.zeros((eH,eW),dtype="uint8")
    offsetX = (eW - image.shape[1]) // 2
    offsetY = (eH - image.shape[0]) // 2
    extent[offsetY:offsetY + image.shape[0],offsetX:offsetX  + image.shape[1]] = image 

    # compute the center of mass of the image and then
    # move the center of mass to the center of the image
    (cY,cX) = np.round(mahotas.center_of_mass(extent)).astype("int32")
    (dX,dY) = ((size[0] // 2) - cX,(size[1] // 2) - cY)
    M = np.float32([[1,0,dX],[0,1,dY]])
    extent = cv2.warpAffine(extent,M,size)

    # return the extent of the image
    return extent 

我们首先定义了center_extent函数,该函数有两个参数。第一个是偏斜校正的图像,第二个是图像的输出尺寸(即输出宽度和高)。

然后检查宽度是否大于图像的高度。如果是这种情况,则会根据图像的宽度调整图像大小。否则,高度大于宽度,因此必须根据图像的高度调整图像大小。

这些都是重要的检查。如果没有进行这些检查并且总是根据图像的宽度调整大小,那么高度可能会大于宽度,因此不适合图像的“extent”。

然后,我们使用相同的维度,给这个extnet的图像分配空间。

接着,我们计算offsetX和offsetY。这些偏移表示图像放置在extent(扩展后)的图像的起始(x,y)坐标(以y,x顺序放置)。

我们使用NumPy数组切片设置实际的extent。

下一步是translate the digit,使其位于图像的中心。

我们使用mahotas包的center_of_mass函数计算图像中白色像素的加权平均值。此函数返回图像中心的加权(x,y)坐标。然后,将这些(x,y)坐标转换为整数而不是浮点数。

然后,我们translates the digit,使其位于图像的中心。

M是我们的平移矩阵,该矩阵告诉我们的图像要进行平移多少像素(从左到右,从上到下)。该矩阵被定义为float32类型的数组,因为OpenCV希望该矩阵是一个float类型。[1,0,tx],其中tx是the number of pixels we will shift the image left or right,而负值则表示图像将向左平移,正值表示图像将向右平移。然后[0,1,ty],其中,ty是the number of pixels we will shift the image up or down。其中,负值表示图像向上平移,正值表示图像向下平移。我们定义好了平移矩阵之后,图像的实际平移是使用了cv2.warpAffine函数来执行,该函数的第一个参数是我们要进行平移的图像,第二个参数是我们的平移矩阵M,最后我们需要手动地提供图像的尺寸(width and height)作为第三个参数。

最后,我们将居中图像返回给调用者。

接下来训练我们的机器模型,编写train.py文件

# import the necessary packages
from sklearn.externals import joblib
from sklearn.svm import LinearSVC
from preprocess.hog import HOG
from preprocess import dataset
import argparse

# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", required = True,
    help = "path to the dataset file")
ap.add_argument("-m", "--model", required = True,
    help = "path to where the model will be stored")
args = vars(ap.parse_args())

首先导入需要的包。我们将使用scikit-learn中的LinearSVC模型来训练线性支持向量机(SVM)。同时还将导入HOG图像描述符和dataset utility functions。最后,argparse将用于解析命令行参数,而joblib将用于将训练过的模型转储到文件中。

我们的脚本需要两个命令行参数,第一个是 –dataset,它是磁盘上的MNIST样本数据集的路径。第二个参数是 –model,是我们训练过的LinearSVC的输出路径。

# load the dataset and initialize the data matrix
(digits,target) = dataset.load_digits(args["dataset"])
data = []

# initialize the HOG descriptor
hog = HOG(orientations=18,pixelsPerCell=(10,10),
    cellsPerBlock=(1,1),transform=True)

#loop over the images 
for image in digits:
    # deskew the image, center it 
    image = dataset.deskew(image,20)
    image = dataset.center_extent(image,(20,20))

    # describe the image and update the data matrix 
    hist = hog.describe(image)
    data.append(hist)

首先我们从磁盘加载由images和targets组成的数据集。然后初始化用于保存每个图像的HOG描述符的数据列表.

接下来,实例化HOG描述符,使用18个orientations作为梯度幅度直方图,每个单元10个像素,每个块1个单元。最后,通过设置transform=True,表示在创建直方图之前将计算像素强度的平方根。

然后开始循环我们的digit images。图像接着被校正并被转换到图像中心。

通过调用describe方法,为预处理图像计算HOG特征向量。最后,使用HOG特征向量更新数据矩阵。

# train the model 
model = LinearSVC(random_state=42)
model.fit(data,target)

# dump the model to file 
joblib.dump(model,args["model"])

最后训练我们的模型。使用伪随机状态42实例化我们的LinearSVC,以确保我们的结果是可重复的。然后使用数据矩阵和target训练模型。最后将我们的模型dump到磁盘里面。

接下来我们可以使用训练好的模型来进行分类,新建classify.py文件

from __future__ import print_function
from sklearn.externals import joblib
from preprocess.hog import HOG
from preprocess import dataset
import argparse
import mahotas
import cv2

# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-m", "--model", required = True,
    help = "path to where the model will be stored")
ap.add_argument("-i", "--image", required = True,
    help = "path to the image file")
args = vars(ap.parse_args())

model = joblib.load(args["model"])

# initialize the HOG descriptor
hog = HOG(orientations = 18, pixelsPerCell = (10, 10),
    cellsPerBlock = (1, 1), transform = True)

首先导入必要的包,然后我们将两个命令行参数传递给classify.py。第一个是–model,即存储cPickle’d模型的路径。第二个–image,是包含我们想要分类和识别的数字的图像的路径。

接着将经过训练的LinearSVC从磁盘加载。

然后,使用与训练阶段期间完全相同的参数来实例化HOG描述符。

现在我们已准备好找到图像中的数字,以便对它们进行分类:

# load the image and convert it to grayscale
image = cv2.imread(args["image"])
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)

# blur the image, find edges, and then find contours along
# the edged regions
blurred = cv2.GaussianBlur(gray,(5,5),0)
edged = cv2.Canny(blurred,30,150)
(_,cnts,_) = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# sort the contours by their x-axis position, ensuring
# that we read the numbers from left to right
cnts = sorted([(c,cv2.boundingRect(c)[0]) for c in cnts],key=lambda x:x[1])

第一步是将查询图像加载到磁盘上,并将其转换为灰度。

接着,使用高斯模糊来模糊图像,并使用Canny边缘检测器在图像中找到边缘。

最后,我们在边缘图像中找到轮廓并从左到右对它们进行排序。这些轮廓中的每一个都代表图像中需要分类的数字。

接下来,我们现在需要处理这些数字中的每一个:

# loop over the contours
for (c,_) in cnts:
    # compute the bouding box for the rectangle
    (x,y,w,h) = cv2.boundingRect(c)

    # if the width is at least 7 pixels and the height
    # is at least 20 pixels, the contour is likely a digit
    if w>=7 and h>=20:
        # crop the ROI and then threshold the grayscale
        # ROI to reveal the digit
        roi = gray[y:y + h,x:x + w]
        thresh = roi.copy()
        T = mahotas.thresholding.otsu(roi)
        thresh[thresh > T] = 255
        thresh = cv2.bitwise_not(thresh)

        # deskew the image center its extent
        thresh = dataset.deskew(thresh, 20)
        thresh = dataset.center_extent(thresh, (20, 20))

        cv2.imshow("thresh", thresh)

接下来我们开始循环我们的轮廓图。使用cv2.boundingRect函数每个轮廓的边界框,该函数返回边界框的起始(x,y)坐标,后跟框的宽度和高度。

然后我们边界框的宽度和高度,以确保它至少有七个像素宽,二十个像素高(视情况而定,比如1的话可能宽度没有那么宽)。如果边界框区域不满足这些尺寸,则认为它太小而不是数字。如果尺寸检查成立,则使用NumPy阵列切片从灰度图像中提取感兴趣区域(ROI)。

此ROI现在保留将被分类的数字。但首先,我们需要应用一些预处理步骤。

首先是应用Otsu的阈值处理方法来分割背景中的前景(数字)(数字写在纸上)。正如在训练阶段一样,数字然后被去偏斜并转换到图像中心。

现在,我们可以对数字进行分类:

# extract features from the image and classify it 
hist = hog.describe(thresh)
digit = model.predict([hist])[0]
print("I think thath number is : {}".format(digit))

# draw a rectangle around the digit, the show what 
# digit was classified as 
cv2.rectangle(image,(x,y),(x + w,y + h),(0,255,0),1)
cv2.putText(image,str(digit),(x-10,y-10),
    cv2.FONT_HERSHEY_SIMPLEX,1.2,(0,255,0),2)
cv2.imshow("Image",image)
cv2.waitKey(0)

首先,我们通过调用HOG描述符的describe方法来计算阈值ROI的HOG特征向量。

HOG特征向量被馈入LinearSVC的预测方法,该方法根据HOG特征向量对ROI进行分类。

然后将分类的数字打印出来。最后在原始图片上显示预测的数字。

我们使用cv2.putText方法在原始图像上绘制数字。cv2.putText函数的第一个参数是我们想要绘制的图像,第二个参数是包含我们想要绘制的字符串。在这种情况下就是我们的数字了。接下来,我们提供将绘制文本的位置的(x,y)坐标。我们希望这个文本在ROI边界框的左边十个像素和上方十个像素。第四个参数是一个内置的OpenCV常量,用于定义将用于绘制文本的字体。第五个参数是文本的相对大小,第六个参数是文本的颜色(绿色),最后一个参数是文本的粗细(两个像素)。

最后执行我们的脚本程序。

python classify.py --model model\svm.cpickle --image images\test1.png

预测结果:

I think thath number is : 8

I think thath number is : 6

I think thath number is : 7

I think thath number is : 4

I think thath number is : 1

完整代码:

链接:https://pan.baidu.com/s/1NBxhzWAcEZDOxX7w99K-kg 提取码:ss6s

更多的参考:

关于Python中的lambda

Python的sort函数和sorted、lambda和cmp

Case Studies – Handwriting Recognition

Getting Started with Deep Learning and Python

LeNet – Convolutional Neural Network in Python


---------------- The End ----------------
支持一下
Fork me on GitHub ;